MemoryPool-Verwendung

von
Thomas Heinrich
oder: Speicherfragmentierung muß nicht sein.

Der Amiga ist gewiss ein sehr effizienter Computer, aber auch nicht perfekt. Manch einer ist sicher schon an dem Problem verzweifelt, daß zwar genug freier Speicher angezeigt wird, sich aber ein bestimmtes Programm einfach nicht laden läßt. Startet man den Amiga neu, funktioniert alles einwandfrei.

Programmierer kennen diesen Effekt wahrscheinlich vor allem aus Bugreports, denn wer programmiert, hat meistens auch mehr als genug freie Resourcen. Schließlich ist der eine oder andere Compiler (GNU...) nicht gerade genügsam.

Seit Kickstart 3.0 gibt es ein System zur Speicherallokierung, die "Memory Pools", das den Speicher nicht mehr so stark fragmentiert, bei richtiger Dimensionierung sogar fast ganz verhindert. Und es kommt noch besser: Sogar Benutzer von Kickstart 1.3 können in den Genuß der Memory Pools kommen, ganz einfach durch Verwendung derselben Pool-Routinen, die auch in der Linker-Library "amiga.lib" vorhanden sind. Wichtig ist, nur die amiga.lib ab der Version 40.15 zu verwenden (also die neueste), da erst ab hier die Funktionen fehlerfrei implementiert sind. Zu finden ist die Library u.a. auf der "Amiga Developer CD" unter "NDK_3.1/Includes&Libs/linker_libs/amiga.lib".

Wer eine frühere Version beutzt, sollte versuchen, die "pools.lib" zu bekommen und diese unbedingt VOR der amiga.lib zu linken. Benutzt man nur die Pool-Funktionen, tut es sogar die pools.lib alleine. Zu finden ist sie z.B. unter "NDK_3.1/Includes&Libs/SetPatch40/pools.lib" auf derselben CD.

Durch die Verwendung der amiga.lib/pools.lib-Funktionen entfällt auch der Check, ob das Programm unter 3.x oder früher läuft, die Routinen übernehmen das nämlich selbst. Man benötigt also keine getrennte Pool-Verwaltung für alte und neue Kickstarts.

Das Prinzip der Memory Pools ist an sich simpel: Statt immer stückchenweise kleine Happen aus dem großen Speicherkuchen zu schneiden, nimmt man nur ein großes Stück und teilt dieses dann, vom Betriebssystem abgeschottet, in die benötigten kleineren Stücke auf. Gibt man den Speicher dann wieder frei, merkt nur der Pool etwas davon - solange, bis man den gesamten Pool wieder freigibt (normalerweise am Programmende). Dann erhält das Betriebssystem den gesamten Block am Stück wieder zurück, ohne die Fragmentierung, die ohne Pool aufgetreten wäre.

Der Programmierer hat sogar noch einen weiteren Vorteil: Selbst vergessene Speicherallokierungen werden mit dem Freigeben des Pools an das System zurückgeliefert. Natürlich nur, wenn sie aus dem Pool allokiert wurden.

Der Nachteil dieser Methode ist ganz offensichtlich, daß man Memory Pools nicht dazu benutzen kann, Speicher zu belegen, der nach Programmende noch belegt sein muß, z.B. für Patches, Handler oder Interrupts. Deshalb sind die nachfolgenden Funktionen auch nur dafür ausgelegt, irgendeinen Speichertyp (Chip oder Fast) zu belegen, der dann am Programmende auf jeden Fall wieder freigegeben wird. Benötigt man anderen Speicher (Chip, Public, 24BitDMA), muß man ihn über AllocMem/AllocVec besorgen.

Die Beispielfunktionen verwenden außerdem die atexit()-Funktion der ANSI C-Library. Man sollte also dafür sorgen, daß der Compiler auch die nötigen Startup-Funktionen benutzt. Die oft gesehene Trickserei mit "_main()" statt "main()" kann hier fatale Folgen haben, je nach Compiler.

Die Includes sind auf MaxonC V3 und 4 abgestimmt, sollten aber leicht anzupassen sein. Schwieriger wir es beim Assemblerinterface für die Pool-Funktionen: Je nach Compiler ist hier mehr Anpassungsarbeit nötig.

Meine Pool-Memory-Funktionen:
BOOL setPool(ULONG size, BOOL debuG);

Diese Funktion erstellt einen Pool mit der Größe <size>. D.h., ein Stück Systemspeicher mit der angegebenen Größe wird belegt. Aus diesem Pool schöpft nun die Funktion "Palloc()" (siehe unten) ihre eigenen, kleineren Speicherteilchen. Ideal wäre es, wenn der maximale Speicherbedarf des Programmes mit diesem einen Stück Speicher abgedeckt wäre. Doch woher soll man das wissen?

Dazu dient die Option <debuG> (ACHTUNG, Schreibweise!). Ist sie "TRUE", dann wird der Pool-Speicherverbrauch mitprotokolliert und kann am Ende des Programms, oder wann immer es nötig erscheint, mit der Funktion "memstats()" (siehe unten) in eine Ausgabedatei ausgegeben werden. So kann man bei einem Testlauf, oder von eventuellen Betatestern, den maximalen Speicherverbrauch auf unterschiedlichen Systemen ermitteln und das fertige Programm kann einen ausreichend großen Pool-Speicher allokieren.

Gibt die Funktion ein "TRUE" zurück, ist alles ok; gibt sie "FALSE" zurück, konnte der Pool nicht erzeugt werden (meistens wegen zu wenig Speicher).

Die Mindestgröße, die ein Pool haben muß, ist mit "MIN_PUDDLE_SIZE" auf ein KByte festgelegt. Diesen Wert kann man an eigene Bedürfnisse anpassen, jedoch ist ein KByte der unterste Grenzwert, ansonsten lohnt sich der Aufwand nicht.

Der Pool und alle daraus erzeugten Allokierungen werden beim Programmende freigegeben. Ich rate davon ab, "exitfunc()" irgendwo aus eigenem Antrieb zu verwenden. Es funktioniert, aber...

ULONG *Palloc(ULONG size);

Diese Funktion wird einfach wie "AllocVec()" benutzt, mit dem Unterschied, daß man keine Speicherflags zu übergeben braucht.

ACHTUNG: Ist <size> größer als der Pool, wird ein extra Speicherblock vom System angefordert. Ist nicht mehr genug freier Speicher im Pool, wird ein neuer Pool mit derselben Größe wie unter "setPool()" angegeben, vom System angefordert.

Der Rückgabewert ist ein Zeiger auf den Speicherbereich, oder "NULL", falls kein Speicher besorgt werden konnte.

HINWEIS: "Palloc()" allokiert immer 4 Bytes mehr als angegeben, um die Länge des Blocks zu speichern. Der Rückgabewert zeigt auf den Nutzteil des Speicherblocks.

NIEMALS MITTELS "Palloc()" ALLOKIERTEN SPEICHER MIT "FreeVec()" FREIGEBEN!

void Pfree(ULONG *mem);

Diese Funktion wird einfach wie "FreeVec()" benutzt. <mem> ist ein Zeiger auf einen mittels "Palloc()" allokierten Speicherbereich.

NIEMALS MITTELS "AllocVec()" ALLOKIERTEN SPEICHER MIT "Pfree()" FREIGEBEN !

ULONG memstats(STRPTR output, STRPTR header, STRPTR footer);

Falls bei der Funktion "setPool()" die Option <debuG> auf "TRUE" gesetzt wurde, gibt diese Funktion eine kleine Statistik über den Pool-Speicherverbrauch aus. Wurde <debuG> nicht gesetzt, sind alle Werte null.

<output> bezeichnet eine Ausgabedatei, z.B. "RAM:poolmem.debug", oder auch "CON:////Debug Output", um eine Konsole zu öffnen. Ist <output> "NULL", dann wird die Standardausgabe (dos library/Output) verwendet.

<header> bezeichnet einen Text, der vor die Ausgabe gestellt wird, z.B. "Memory Pools Output of Program MyFantasticProg\n\n"

<footer> bezeichnet einen Text, der nach die Ausgabe gestellt wird, z.B. "\nPlease report this output to programmer@beta.test.com\n"

Jede Zeile benötigt eine eigene Formatierung, normalerweise genügt ein "\n" am Ende, man kann aber auch mehrere Zeilen ausgeben.

Ein Beispiel sähe so aus:

   Memory Pools Output of Program MyFantasticProg

   Maximum memory used:      73088 Bytes
   Largest allocation:       16020 Bytes
   Forgotten memory:           488 Bytes
   Memory pool is valid !

   Please report this output to programmer@beta.test.com

In diesem Fall könnte man den Pool mittels "setPool(74000);" einrichten, falls andere Betatester auf mehr Speicherverbrauch kommen, entsprechend höher. Die Werte müssen aber sinnvoll sein.

Braucht das Programm bei einzelnen Usern z.B. 4 MByte, dann werden User mit weniger als 4 MByte freiem RAM nichts mit dem Programm anfangen können, da der Pool nicht erstellt werden kann.

Ich empfehle, "memstats()" per Tooltype oder Shell-Option zugänglich zu machen, zusätzlich zu einer Angabe des <output>-Parameters, z.B. DEBUG=on DEBUGOUTPUT=debugfile. So kann auch in der entgültigen Version einem User besser Hilfestellung gewährt werden.

Der Quelltext der vorgestellten Funktionen ist im Binärarchiv verfügbar.


Prev Inhaltsverzeichnis Next
©`98Der AmZeiger